use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;

use irc::connection;
use serde::{Deserialize, Deserializer};

use crate::config;
use crate::serde::{
    deserialize_path_buf_with_path_transformations,
    deserialize_path_buf_with_path_transformations_maybe,
};

const DEFAULT_PORT: u16 = 6667;
const DEFAULT_TLS_PORT: u16 = 6697;

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct Server {
    /// The client's nickname.
    pub nickname: String,
    /// The client's NICKSERV password.
    pub nick_password: Option<String>,
    /// The client's NICKSERV password file.
    #[serde(
        deserialize_with = "deserialize_path_buf_with_path_transformations_maybe"
    )]
    pub nick_password_file: Option<PathBuf>,
    /// Truncate read from NICKSERV password file to first newline
    pub nick_password_file_first_line_only: bool,
    /// The client's NICKSERV password command.
    pub nick_password_command: Option<String>,
    /// The server's NICKSERV IDENTIFY syntax.
    pub nick_identify_syntax: Option<IdentifySyntax>,
    /// Alternative nicknames for the client, if the default is taken.
    pub alt_nicks: Vec<String>,
    /// The client's username.
    pub username: Option<String>,
    /// The client's real name.
    pub realname: Option<String>,
    /// The server to connect to.
    pub server: String,
    /// The port to connect on.
    pub port: u16,
    /// The password to connect to the server.
    pub password: Option<String>,
    /// The file with the password to connect to the server.
    #[serde(
        deserialize_with = "deserialize_path_buf_with_path_transformations_maybe"
    )]
    pub password_file: Option<PathBuf>,
    /// Truncate read from password file to first newline
    pub password_file_first_line_only: bool,
    /// The command which outputs a password to connect to the server.
    pub password_command: Option<String>,
    /// Filter settings for the server, e.g. ignored nicks
    pub filters: Option<Filters>,
    /// A list of channels to join on connection.
    pub channels: Vec<String>,
    /// A mapping of channel names to keys for join-on-connect.
    pub channel_keys: HashMap<String, String>,
    /// The amount of inactivity in seconds before the client will ping the server.
    pub ping_time: u64,
    /// The amount of time in seconds for a client to reconnect due to no ping response.
    pub ping_timeout: u64,
    /// The amount of time in seconds before attempting to reconnect to the server when disconnected.
    pub reconnect_delay: u64,
    /// Whether the client should use NickServ GHOST to reclaim its primary nickname if it is in
    /// use. This has no effect if `nick_password` is not set.
    pub should_ghost: bool,
    /// The command(s) that should be sent to NickServ to recover a nickname. The nickname and
    /// password will be appended in that order after the command.
    /// E.g. `["RECOVER", "RELEASE"]` means `RECOVER nick pass` and `RELEASE nick pass` will be sent
    /// in that order.
    pub ghost_sequence: Vec<String>,
    /// User modestring to set on connect. Example: "+RB-x"
    pub umodes: Option<String>,
    /// Whether or not to use TLS.
    /// Clients will automatically panic if this is enabled without TLS support.
    pub use_tls: bool,
    /// On `true`, all certificate validations are skipped. Defaults to `false`.
    pub dangerously_accept_invalid_certs: bool,
    /// The path to the root TLS certificate for this server in PEM format.
    #[serde(
        deserialize_with = "deserialize_path_buf_with_path_transformations_maybe"
    )]
    root_cert_path: Option<PathBuf>,
    /// Sasl authentication
    pub sasl: Option<Sasl>,
    /// Commands which are executed once connected.
    pub on_connect: Vec<String>,
    /// Enable WHO polling. Defaults to `true`.
    pub who_poll_enabled: bool,
    /// WHO poll interval for servers without away-notify.
    #[serde(deserialize_with = "deserialize_who_poll_interval")]
    pub who_poll_interval: Duration,
    /// A list of nicknames to monitor (if MONITOR is supported by the server).
    pub monitor: Vec<String>,
    pub chathistory: bool,
    #[serde(deserialize_with = "deserialize_anti_flood")]
    pub anti_flood: Duration,
    #[serde(skip)]
    pub order: u16,
    pub proxy: Option<config::Proxy>,
}

impl Server {
    pub fn new(
        server: String,
        port: Option<u16>,
        nickname: String,
        channels: Vec<String>,
        use_tls: bool,
    ) -> Self {
        Self {
            nickname,
            server,
            port: port.unwrap_or(if use_tls {
                DEFAULT_TLS_PORT
            } else {
                DEFAULT_PORT
            }),
            channels,
            use_tls,
            dangerously_accept_invalid_certs: false,
            ..Default::default()
        }
    }

    pub fn connection(
        &self,
        proxy: Option<config::Proxy>,
    ) -> connection::Config<'_> {
        let security = if self.use_tls {
            connection::Security::Secured {
                accept_invalid_certs: self.dangerously_accept_invalid_certs,
                root_cert_path: self.root_cert_path.as_ref(),
                client_cert_path: self
                    .sasl
                    .as_ref()
                    .and_then(Sasl::external_cert),
                client_key_path: self
                    .sasl
                    .as_ref()
                    .and_then(Sasl::external_key),
            }
        } else {
            connection::Security::Unsecured
        };

        connection::Config {
            server: &self.server,
            port: self.port,
            security,
            proxy: proxy.map(From::from),
        }
    }

    pub fn bouncer_config(&self) -> Self {
        Self {
            // nickserv info not relevant to the bounced network
            nick_password_file: Option::default(),
            nick_password_command: Option::default(),
            nick_identify_syntax: Option::default(),

            // channels not relevant
            channels: Vec::default(),
            channel_keys: HashMap::default(),

            // ghost sequence not relevant
            should_ghost: Default::default(),
            ghost_sequence: Server::default().ghost_sequence,

            ..self.clone()
        }
    }
}

impl Default for Server {
    fn default() -> Self {
        Self {
            nickname: String::default(),
            nick_password: Option::default(),
            nick_password_file: Option::default(),
            nick_password_file_first_line_only: true,
            nick_password_command: Option::default(),
            nick_identify_syntax: Option::default(),
            alt_nicks: Vec::default(),
            username: Option::default(),
            realname: Option::default(),
            server: String::default(),
            port: DEFAULT_TLS_PORT,
            password: Option::default(),
            password_file: Option::default(),
            password_file_first_line_only: true,
            password_command: Option::default(),
            filters: Option::default(),
            channels: Vec::default(),
            channel_keys: HashMap::default(),
            ping_time: 180,
            ping_timeout: 20,
            reconnect_delay: 10,
            should_ghost: Default::default(),
            ghost_sequence: vec!["REGAIN".into()],
            umodes: Option::default(),
            use_tls: true,
            dangerously_accept_invalid_certs: Default::default(),
            root_cert_path: Option::default(),
            sasl: Option::default(),
            on_connect: Vec::default(),
            who_poll_enabled: true,
            who_poll_interval: Duration::from_secs(2),
            monitor: Vec::default(),
            chathistory: true,
            anti_flood: Duration::from_millis(2000),
            order: 0,
            proxy: None,
        }
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IdentifySyntax {
    NickPassword,
    PasswordNick,
}

#[derive(PartialEq, Eq, Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Sasl {
    Plain {
        /// Account name
        username: String,
        /// Account password,
        password: Option<String>,
        /// Account password file
        #[serde(
            default,
            deserialize_with = "deserialize_path_buf_with_path_transformations_maybe"
        )]
        password_file: Option<PathBuf>,
        /// Truncate read from password file to first newline
        password_file_first_line_only: Option<bool>,
        /// Account password command
        password_command: Option<String>,
    },
    External {
        /// The path to PEM encoded X509 user certificate for external auth
        #[serde(
            deserialize_with = "deserialize_path_buf_with_path_transformations"
        )]
        cert: PathBuf,
        /// The path to PEM encoded PKCS#8 private key corresponding to the user certificate for external auth
        #[serde(
            default,
            deserialize_with = "deserialize_path_buf_with_path_transformations_maybe"
        )]
        key: Option<PathBuf>,
    },
}

impl Sasl {
    pub fn command(&self) -> &'static str {
        match self {
            Sasl::Plain { .. } => "PLAIN",
            Sasl::External { .. } => "EXTERNAL",
        }
    }

    pub fn params(&self) -> Vec<String> {
        const CHUNK_SIZE: usize = 400;

        match self {
            Sasl::Plain {
                username, password, ..
            } => {
                use base64::engine::Engine;

                let password = password
                    .as_ref()
                    .expect("SASL password must exist at this point!");

                // Exclude authorization ID, to use the authentication ID as the authorization ID
                // https://datatracker.ietf.org/doc/html/rfc4616#section-2
                let encoding = base64::engine::general_purpose::STANDARD
                    .encode(format!("\x00{username}\x00{password}"));

                let chunks = encoding
                    .as_bytes()
                    .chunks(CHUNK_SIZE)
                    .collect::<Vec<&[u8]>>();

                let signal_end_of_response = chunks
                    .iter()
                    .last()
                    .is_none_or(|chunk| chunk.len() == CHUNK_SIZE);

                let mut params = chunks
                    .into_iter()
                    .map(|chunk| {
                        String::from_utf8(chunk.into())
                            .expect("chunks should be valid UTF-8")
                    })
                    .collect::<Vec<String>>();

                if signal_end_of_response {
                    params.push("+".into());
                }

                params
            }
            Sasl::External { .. } => vec!["+".into()],
        }
    }

    fn external_cert(&self) -> Option<&PathBuf> {
        if let Self::External { cert, .. } = self {
            Some(cert)
        } else {
            None
        }
    }

    fn external_key(&self) -> Option<&PathBuf> {
        if let Self::External { key, .. } = self {
            key.as_ref()
        } else {
            None
        }
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct Filters {
    pub ignore: Vec<String>,
}

fn deserialize_anti_flood<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
    D: Deserializer<'de>,
{
    let milliseconds: u64 = Deserialize::deserialize(deserializer)?;

    if !(100..=60000).contains(&milliseconds) {
        Err(serde::de::Error::invalid_value(
            serde::de::Unexpected::Unsigned(milliseconds),
            &"integer in the range 100 .. 60000",
        ))
    } else {
        Ok(Duration::from_millis(milliseconds))
    }
}

fn deserialize_who_poll_interval<'de, D>(
    deserializer: D,
) -> Result<Duration, D::Error>
where
    D: Deserializer<'de>,
{
    let seconds: u64 = Deserialize::deserialize(deserializer)?;

    if !(1..=3600).contains(&seconds) {
        Err(serde::de::Error::invalid_value(
            serde::de::Unexpected::Unsigned(seconds),
            &"integer in the range 1 .. 3600",
        ))
    } else {
        Ok(Duration::from_secs(seconds))
    }
}
